Skip to content

Conversation

@kbrilla
Copy link

@kbrilla kbrilla commented Jan 25, 2026

Type Hierarchy Feature: Comprehensive Enhancement

Summary

This PR implements a comprehensive Type Hierarchy feature for the TypeScript language service, providing IDE support for navigating class, interface, and type alias hierarchies. The feature supports the LSP Type Hierarchy protocol and enables rich navigation across supertypes and subtypes.

Fixes #45877

Features

Core Functionality

  • Prepare Type Hierarchy: Resolves the type hierarchy declaration at a given position
  • Provide Supertypes: Returns base classes, implemented interfaces, and referenced types
  • Provide Subtypes: Returns derived classes, implementing classes, and intersection type aliases
  • On-Demand Loading: Following LSP protocol, the tree is loaded lazily - one level at a time when expanded in the UI

Supported Declaration Types

  1. Classes (named and anonymous class expressions)
  2. Interfaces (including declaration merging)
  3. Type Aliases (with semantic relationship tracking)
  4. Mixin Variables (const Mixed = Mixin(Base) patterns)
  5. Type Parameters (with constraint tracking)

Advanced Type Patterns

The implementation handles complex TypeScript patterns:

Pattern Supertype Support Subtype Support
extends clauses
implements clauses
Intersection types (A & B) ✅ (members) ✅ (as structural subtype)
Union types (A | B) ✅ (members) ❌ (semantically incorrect)
Conditional types ✅ (extends clause) ✅ (as possible subtype)
Mapped types ✅ (referenced types) ❌ (see note below)
Generic instantiations ✅ (original generic)
Re-exports/module aliases
Mixin patterns ✅ (composition chain) ✅ (reverse lookup)
Type parameter constraints N/A

Note on Mapped Types: Utility types like Required<T>, Pick<T, K>, Partial<T> are type-level transformations, not inheritance relationships. They don't appear as subtypes because structural subtype checks are expensive and can produce noisy results. The semantics vary - some create subtypes (Required), some create supertypes (Partial).

Kind Modifiers

Types are annotated with descriptive kindModifiers to help distinguish different type relationships:

Modifier Description Example
mixin Mixin variable const Mixed = Mixin(Base)
alias Simple type alias type Foo = Bar
conditional,extends Simple conditional type T extends U ? X : Y
conditional,infer Inference conditional type T extends (...) => infer R ? R : never
intersection Intersection type A & B
union Union type A | B
mapped Mapped type { [K in keyof T]: ... }
tuple Tuple type [A, B]
template Template literal type `Hello ${string}`
indexed Indexed access type T["key"]
keyof Keyof operator keyof T
readonly Readonly operator readonly T[]

Configurable Result Limits

The maximum number of results per level is configurable via UserPreferences:

// In UserPreferences
typeHierarchyMaxResults?: number; // Default: 1000

This allows clients to adjust the limit based on their needs and prevents performance issues in very large codebases.

Implementation Details

Files Changed

  1. src/services/typeHierarchy.ts (new, ~1000 lines)

    • Core type hierarchy implementation
    • resolveTypeHierarchyDeclaration() - Entry point for type hierarchy requests
    • createTypeHierarchyItem() - Creates hierarchy items with proper metadata
    • getSupertypes() - Collects base types, implemented interfaces, and type parameter constraints
    • getSubtypes() - Finds derived types using hybrid approach with configurable limits
    • getTypeHierarchyKindModifiers() - Returns modifiers based on type pattern
    • findMixinVariablesUsingSymbol() - Reverse mixin lookup
    • collectTypeParameterConstraints() - Collects type parameter constraints as supertypes
  2. src/compiler/types.ts (modified)

    • Added typeHierarchyMaxResults to UserPreferences for configurable result limits
  3. src/services/types.ts (modified)

    • Updated LanguageService interface with preferences parameter
  4. src/services/services.ts (modified)

    • Pass preferences to type hierarchy functions
  5. src/server/session.ts (modified)

    • Pass user preferences to language service calls
  6. src/harness/fourslashImpl.ts (modified)

    • Added kindModifiers display in type hierarchy baselines
    • Added helper functions for formatting
  7. Test Files (27 new fourslash tests)

    • Comprehensive coverage of all supported patterns
    • Multi-file tests for cross-file scenarios
    • Edge case and negative case testing

Key Algorithms

Supertype Collection

  • Uses getEffectiveBaseTypeNode() for class inheritance
  • Iterates heritage clauses for extends/implements
  • Handles type aliases by analyzing their type structure
  • Traces mixin composition chains recursively
  • Collects type parameter constraints

Subtype Collection (Hybrid Approach)

  1. FindAllReferences with { implementations: true } for efficient heritage clause lookup
  2. Manual traversal for type alias subtypes (intersection types only - can't be found via FindAllReferences)
  3. Reverse mixin lookup for finding mixin variables that use a base class
  4. Results limit (configurable, default 1000) to prevent performance issues

Symbol Resolution

  • Properly resolves import aliases using skipAlias()
  • Handles declaration merging via getMergedSymbol()
  • Resolves generic type instantiations to their base types

Mixin Support

The implementation recognizes and supports TypeScript mixin patterns:

// Mixin function
function Timestamped<T extends new (...args: any[]) => any>(Base: T) {
    return class extends Base { timestamp = Date.now(); };
}

// Mixin variable - recognized as TypeHierarchyDeclaration
const TimestampedUser = Timestamped(User);

// Querying MixinBase finds all mixin variables using it
export class MixinBase {}
export const Full = Serializable(Activatable(Timestamped(MixinBase)));
// MixinBase → subtypes: [TimestampedBase, ActivatableTimestamped, FullMixin]

Type Parameter Constraints

Type hierarchy shows type parameter constraints as supertypes:

interface Entity { id: number; }
function process<T extends Entity>(item: T) { ... }
// T → supertypes: [Entity]

Testing

Test Coverage

  • 27 fourslash tests covering:
    • Basic class/interface inheritance
    • Abstract classes
    • Multi-file scenarios
    • Generic types and instantiations
    • Type parameter constraints
    • Conditional types
    • Mapped types
    • Intersection/union types
    • Template literal types
    • Indexed access types
    • Mixin patterns (forward and reverse lookup)
    • Re-exports and module aliases
    • Declaration merging
    • Project references
    • Edge cases and negative cases

Running Tests

# Run all type hierarchy tests
npx hereby runtests --tests=typeHierarchy

# Run specific test
npx hereby runtests --tests=tests/cases/fourslash/typeHierarchyMixinsDeclarations.ts

# Run full test suite
npx hereby runtests-parallel

Semantic Correctness

Union vs Intersection Types

The implementation correctly models type relationships:

  • Intersection types (A & B): A subtype of both A and B (has ALL properties)
  • Union types (A | B): A supertype of A and B (not a subtype!)
type Pet = Dog | Cat;  // Pet is NOT a subtype of Dog - relationship reversed!
type DogPet = Dog & Pet;  // DogPet IS a subtype of both Dog and Pet

Conditional Types

Conditional types like ExtractDog<T> = T extends Dog ? T : never are shown as "possible subtypes" when the condition could be satisfied. The conditional,extends or conditional,infer modifiers help distinguish these from structural subtypes.

Mapped Types

Mapped types like Required<T>, Partial<T>, Pick<T, K> are NOT shown as subtypes because:

  1. They are type-level transformations, not inheritance relationships
  2. Structural subtype checks are expensive and produce noisy results
  3. Semantics vary - Required<T> creates subtypes, Partial<T> creates supertypes

Performance Considerations

  • On-Demand Loading: Follows LSP protocol - tree is loaded one level at a time as user expands nodes
  • FindAllReferences: Uses efficient name index for subtype lookup
  • AST Traversal: Avoided when possible; uses targeted lookups
  • Seen Sets: Prevents duplicate processing
  • Cancellation Tokens: Supports responsiveness during long operations
  • Configurable Results Limit: Subtype collection is capped at a configurable limit (default: 1000) per level

Known Limitations

  1. Deeply nested generics: May not fully trace through complex generic type transformations
  2. Type-level computation: Runtime-computed types (e.g., complex mapped types) may not show all relationships
  3. Anonymous classes from mixins: Mixin functions return anonymous classes; we show the mixin variable instead
  4. satisfies expressions: Not applicable - satisfies is an expression-level operator, not a type-level relationship
  5. Mapped type subtypes: Utility types like Required/Partial/Pick not shown as subtypes (see note above)

Future Enhancements

  • Truncation indicator - Show indicator when results exceed the configured limit
  • Handle more complex generic type inference
  • Show inferred type widening in hierarchy
  • Enhanced cross-project reference support

AI Disclosure

This PR was developed with assistance from GitHub Copilot (Claude Opus 4.5). The AI helped with:

  • Code implementation and algorithm design
  • Test case creation and coverage analysis
  • Documentation and PR description writing
  • Code review and bug identification

All code has been reviewed by the contributor @kbrilla and tested against the full TypeScript test suite (99,267 tests passing).

Implements the LSP Type Hierarchy protocol with support for:

Core Features:
- prepareTypeHierarchy: resolves declarations at cursor position
- supertypes: returns base classes, interfaces, and type parameter constraints
- subtypes: returns derived classes, implementing classes, and intersection types
- On-demand loading: tree loads lazily one level at a time per LSP protocol

Supported Declaration Types:
- Classes (named and anonymous class expressions)
- Interfaces (including declaration merging)
- Type aliases (with semantic relationship tracking)
- Mixin variables (const Mixed = Mixin(Base) patterns)
- Type parameters (with constraint tracking)

Kind Modifiers:
- mixin, alias, conditional,extends, conditional,infer
- intersection, union, mapped, tuple, template
- indexed, keyof, readonly

Performance:
- FindAllReferences for efficient subtype lookup
- Results limit (1000) to prevent issues in large codebases
- Cancellation token support

Tests: 27 fourslash tests covering all patterns
All 99,267 tests passing

AI Disclosure: Developed with GitHub Copilot (Claude Opus 4.5) assistance
Copilot AI review requested due to automatic review settings January 25, 2026 15:49
@github-project-automation github-project-automation bot moved this to Not started in PR Backlog Jan 25, 2026
@typescript-bot
Copy link
Collaborator

Thanks for the PR! It looks like you've changed the TSServer protocol in some way. Please ensure that any changes here don't break consumers of the current TSServer API. For some extra review, we'll ping @sheetalkamat, @mjbvz, and @joj for you. Feel free to loop in other consumers/maintainers if necessary.

@typescript-bot
Copy link
Collaborator

Looks like you're introducing a change to the public API surface area. If this includes breaking changes, please document them on our wiki's API Breaking Changes page.

Also, please make sure @DanielRosenwasser and @RyanCavanaugh are aware of the changes, just as a heads up.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements a comprehensive Type Hierarchy feature for the TypeScript language service, enabling IDE support for navigating class, interface, and type alias hierarchies through LSP Type Hierarchy protocol support.

Changes:

  • Added core type hierarchy functionality supporting classes, interfaces, type aliases, mixins, and type parameters
  • Implemented on-demand lazy loading following LSP protocol with supertype/subtype navigation
  • Created comprehensive test suite with 27 fourslash tests covering various TypeScript patterns

Reviewed changes

Copilot reviewed 42 out of 66 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/services/typeHierarchy.ts New core implementation file (~1000 lines) with hierarchy resolution, supertype/subtype collection, and mixin pattern support
src/services/types.ts Added TypeHierarchyItem interface and three new LanguageService methods for type hierarchy protocol
src/services/services.ts Integrated type hierarchy methods into language service with proper synchronization
src/services/_namespaces/* Added TypeHierarchy namespace exports
src/server/session.ts Added server-side protocol handlers for type hierarchy requests
src/server/protocol.ts Added protocol definitions for type hierarchy commands and responses
src/harness/fourslashImpl.ts Added test harness support with baseline formatting for type hierarchy
src/harness/fourslashInterfaceImpl.ts Added verify API method for baseline testing
src/harness/client.ts Added client-side type hierarchy methods with protocol conversion
tests/cases/fourslash/* 27 new comprehensive test files covering all supported patterns
tests/baselines/reference/* Baseline files for type hierarchy test outputs

Comment on lines 4430 to 4437
private formatTypeHierarchyItemSpan(file: FourSlashFile | undefined, item: ts.TypeHierarchyItem, span: ts.TextSpan, prefix: string, trailingPrefix = prefix) {
// For lib files, we don't have the source available in the test
if (!file) {
let text = "";
text += `${prefix}${item.file} (lib file)\n`;
text += `${prefix}│ <source not available>\n`;
text += `${trailingPrefix}╰\n`;
return text;
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method name formatTypeHierarchyItemSpan is inconsistent with its implementation. It handles type hierarchy items but delegates to formatCallHierarchyItemSpan for non-lib files. Consider renaming to better reflect that it's a wrapper/adapter, or extract the lib file handling into a separate helper method.

Suggested change
private formatTypeHierarchyItemSpan(file: FourSlashFile | undefined, item: ts.TypeHierarchyItem, span: ts.TextSpan, prefix: string, trailingPrefix = prefix) {
// For lib files, we don't have the source available in the test
if (!file) {
let text = "";
text += `${prefix}${item.file} (lib file)\n`;
text += `${prefix}│ <source not available>\n`;
text += `${trailingPrefix}╰\n`;
return text;
private formatLibFileHierarchyItemSpan(item: ts.TypeHierarchyItem, prefix: string, trailingPrefix: string): string {
// For lib files, we don't have the source available in the test
let text = "";
text += `${prefix}${item.file} (lib file)\n`;
text += `${prefix}│ <source not available>\n`;
text += `${trailingPrefix}╰\n`;
return text;
}
private formatTypeHierarchyItemSpan(file: FourSlashFile | undefined, item: ts.TypeHierarchyItem, span: ts.TextSpan, prefix: string, trailingPrefix = prefix) {
if (!file) {
return this.formatLibFileHierarchyItemSpan(item, prefix, trailingPrefix);

Copilot uses AI. Check for mistakes.

public baselineTypeHierarchy(): void {
const item = this.languageService.prepareTypeHierarchy(this.activeFile.fileName, this.currentCaretPosition);
const text = item ? ts.mapOneOrMany(item, i => this.formatTypeHierarchy(i), result => result.join("")) : "none";
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable name i is too ambiguous for a callback parameter that represents a TypeHierarchyItem. Consider renaming to hierarchyItem or item for better clarity.

Suggested change
const text = item ? ts.mapOneOrMany(item, i => this.formatTypeHierarchy(i), result => result.join("")) : "none";
const text = item ? ts.mapOneOrMany(item, hierarchyItem => this.formatTypeHierarchy(hierarchyItem), result => result.join("")) : "none";

Copilot uses AI. Check for mistakes.
function provideTypeHierarchySupertypes(fileName: string, position: number): TypeHierarchyItem[] {
synchronizeHostData();
const sourceFile = getValidSourceFile(fileName);
const declaration = TypeHierarchy.resolveTypeHierarchyDeclaration(program, position === 0 ? sourceFile : getTouchingPropertyName(sourceFile, position));
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The expression position === 0 ? sourceFile : getTouchingPropertyName(sourceFile, position) is duplicated in both provideTypeHierarchySupertypes and provideTypeHierarchySubtypes. Consider extracting this into a helper function to reduce code duplication.

Copilot uses AI. Check for mistakes.
Comment on lines 3374 to 3384
function provideTypeHierarchySupertypes(fileName: string, position: number): TypeHierarchyItem[] {
synchronizeHostData();
const sourceFile = getValidSourceFile(fileName);
const declaration = TypeHierarchy.resolveTypeHierarchyDeclaration(program, position === 0 ? sourceFile : getTouchingPropertyName(sourceFile, position));
return declaration ? TypeHierarchy.getSupertypes(program, declaration, cancellationToken) : [];
}

function provideTypeHierarchySubtypes(fileName: string, position: number): TypeHierarchyItem[] {
synchronizeHostData();
const sourceFile = getValidSourceFile(fileName);
const declaration = TypeHierarchy.resolveTypeHierarchyDeclaration(program, position === 0 ? sourceFile : getTouchingPropertyName(sourceFile, position));
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The expression position === 0 ? sourceFile : getTouchingPropertyName(sourceFile, position) is duplicated in both provideTypeHierarchySupertypes and provideTypeHierarchySubtypes. Consider extracting this into a helper function to reduce code duplication.

Suggested change
function provideTypeHierarchySupertypes(fileName: string, position: number): TypeHierarchyItem[] {
synchronizeHostData();
const sourceFile = getValidSourceFile(fileName);
const declaration = TypeHierarchy.resolveTypeHierarchyDeclaration(program, position === 0 ? sourceFile : getTouchingPropertyName(sourceFile, position));
return declaration ? TypeHierarchy.getSupertypes(program, declaration, cancellationToken) : [];
}
function provideTypeHierarchySubtypes(fileName: string, position: number): TypeHierarchyItem[] {
synchronizeHostData();
const sourceFile = getValidSourceFile(fileName);
const declaration = TypeHierarchy.resolveTypeHierarchyDeclaration(program, position === 0 ? sourceFile : getTouchingPropertyName(sourceFile, position));
function resolveTypeHierarchyDeclarationAtPosition(program: Program, sourceFile: SourceFile, position: number) {
const node = position === 0 ? sourceFile : getTouchingPropertyName(sourceFile, position);
return TypeHierarchy.resolveTypeHierarchyDeclaration(program, node);
}
function provideTypeHierarchySupertypes(fileName: string, position: number): TypeHierarchyItem[] {
synchronizeHostData();
const sourceFile = getValidSourceFile(fileName);
const declaration = resolveTypeHierarchyDeclarationAtPosition(program, sourceFile, position);
return declaration ? TypeHierarchy.getSupertypes(program, declaration, cancellationToken) : [];
}
function provideTypeHierarchySubtypes(fileName: string, position: number): TypeHierarchyItem[] {
synchronizeHostData();
const sourceFile = getValidSourceFile(fileName);
const declaration = resolveTypeHierarchyDeclarationAtPosition(program, sourceFile, position);

Copilot uses AI. Check for mistakes.
- Extract formatLibFileHierarchyItemSpan helper for lib file handling
- Rename 'i' to 'hierarchyItem' for better clarity
- Extract getTypeHierarchyNodeAtPosition to reduce duplication
@kbrilla
Copy link
Author

kbrilla commented Jan 25, 2026

@microsoft-github-policy-service agree

@typescript-bot typescript-bot added the For Backlog Bug PRs that fix a backlog bug label Jan 25, 2026
- Add typeHierarchyMaxResults to UserPreferences (default: 1000)
- Update type hierarchy API to accept preferences parameter
- Pass preferences through services.ts and session.ts
- Use configurable limit in getSubtypes for performance control
- Document in PR_DESCRIPTION.md
This file is for PR documentation only, not part of the codebase
Document why mapped types like Required<T>, Pick<T, K> are not shown as subtypes:
- Structural subtype checks are expensive and can produce noisy results
- Mapped type semantics vary - some create subtypes, some create supertypes
@jakebailey
Copy link
Member

Just so you don't spend more effort on this, we wouldn't accept this PR: #62827

You could theoretically send this to https://github.com/microsoft/typescript-go which actually has the LSP and everything in place to do something like this, or wait until this repo becomes the Go code.

@kbrilla
Copy link
Author

kbrilla commented Jan 25, 2026

You could theoretically send this to https://github.com/microsoft/typescript-go which actually has the LSP and everything in place to do something like this, or wait until this repo becomes the Go code.

yes, I can do that, thx for the info!

@kbrilla kbrilla requested a review from Copilot January 25, 2026 17:17
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 44 out of 67 changed files in this pull request and generated 3 comments.

}

private formatTypeHierarchyItem(file: FourSlashFile | undefined, item: ts.TypeHierarchyItem, direction: TypeHierarchyItemDirection, seen: Map<string, boolean>, prefix: string, trailingPrefix: string = prefix): string {
const key = `${item.file}|${JSON.stringify(item.span)}|${direction}`;
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using JSON.stringify for creating cache keys can be expensive when called frequently. Consider using a simpler concatenation like ${item.file}|${item.span.start}|${item.span.length}|${direction} for better performance.

Suggested change
const key = `${item.file}|${JSON.stringify(item.span)}|${direction}`;
const key = `${item.file}|${item.span.start}|${item.span.length}|${direction}`;

Copilot uses AI. Check for mistakes.
Comment on lines +3374 to +3376
function getTypeHierarchyNodeAtPosition(sourceFile: SourceFile, position: number): Node {
return position === 0 ? sourceFile : getTouchingPropertyName(sourceFile, position);
}
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This helper function duplicates logic from the call hierarchy implementation. Consider extracting a shared utility function to reduce code duplication.

Copilot uses AI. Check for mistakes.

public baselineTypeHierarchy(): void {
const item = this.languageService.prepareTypeHierarchy(this.activeFile.fileName, this.currentCaretPosition);
const text = item ? ts.mapOneOrMany(item, hierarchyItem => this.formatTypeHierarchy(hierarchyItem), result => result.join("")) : "none";
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inline arrow function and join callback make this line difficult to read. Consider breaking this into multiple lines or extracting the mapping logic for better clarity.

Suggested change
const text = item ? ts.mapOneOrMany(item, hierarchyItem => this.formatTypeHierarchy(hierarchyItem), result => result.join("")) : "none";
let text: string;
if (item) {
const formatHierarchyItem = (hierarchyItem: ts.TypeHierarchyItem) => this.formatTypeHierarchy(hierarchyItem);
const concatenateResults = (results: string[]) => results.join("");
text = ts.mapOneOrMany(item, formatHierarchyItem, concatenateResults);
}
else {
text = "none";
}

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

For Backlog Bug PRs that fix a backlog bug

Projects

Status: Not started

Development

Successfully merging this pull request may close these issues.

Type hierarchy api support

3 participants